/*
 *  Copyright 2012 Iurii Dunko, Iana Potochniak.
 * 
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *  under the License.
 */
package org.opu.yyminijava.codegeneration;

import org.opu.yyminijava.util.CodeExecuteException;
import org.opu.yyminijava.util.CodeReadException;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import org.opu.yyminijava.symboltable.SymbolTable;
import org.opu.yyminijava.util.ArchiveUtil;
import org.simpleframework.xml.core.Persister;

/**
 *
 * @author Iurii Dunko
 * @author Iana Potochniak
 */
public class ClassRepository implements Serializable{

    private static final long serialVersionUID = -5189760030418031042L;
	
    /*
     * 1st - Normal java, when generate byte code, read program file 2 times
     * first time to create symbol table, whe don't store it to...
     * 2nd - Class name cannot start with digit, so system table name is 0st.
     * 3d - The serialisation is based on XML (you can recompile).
     */

    public static final String CLASS_EXTENSION = ".xml"; // ".cbc"
    public static final String SYMBOL_TABLE_NAME = "0st" + CLASS_EXTENSION;

    private Map<String, ClassFile> classes;
    private SymbolTable symbolTable;

    static ClassFile createSystemClass() {
        ClassFile systemClass = new ClassFile("System");

        Method printlnMethod = new Method("out.println");
        printlnMethod.setStaticModifier(true);
        printlnMethod.addInstruction(ICode.PRINTLN, null);
        systemClass.addMethod("out.println", printlnMethod);

        Method printMethod = new Method("out.print");
        printMethod.addInstruction(ICode.PRINT, null);
        printMethod.setStaticModifier(true);
        systemClass.addMethod("out.print", printMethod);

        return systemClass;
    }

    public ClassRepository(Map<String, ClassFile> classes, SymbolTable symbolTable) {
        this.classes = classes;
        this.symbolTable = symbolTable;
        this.classes.put("System", createSystemClass());
    }

    public ClassRepository(File file) throws FileNotFoundException, IOException {
        this(new FileInputStream(file));
    }

    public ClassRepository(InputStream is) throws IOException {
        read(is);
    }

    public Method getMethod(String qName) {
        String[] split = qName.split("\\.", 2);
        ClassFile classFile = getClassFile(split[0]);
        return classFile.getMethod(split[1]);
    }

    public ClassFile getClassFile(String classFileName) {
        if (!classes.containsKey(classFileName)) {
            throw new CodeReadException("Don't have class " + classFileName);
        }

        return classes.get(classFileName);
    }

    public SymbolTable getSymbolTable() {
        return symbolTable;
    }
    
    public void print(){
    	for(String name: classes.keySet()){
    		classes.get(name).print();
    	}
    }

    public final void read(InputStream is) throws IOException {
        Map<String, byte[]> archive = ArchiveUtil.readArchive(is);

        // We create map instance here because archive may be cannot be read
        // and we would get error, so we won't be need to create instance at all
        classes = new HashMap<String, ClassFile>();

        for (String ma : archive.keySet()) {
            if (SYMBOL_TABLE_NAME.equals(ma)) {
            	// TODO: Yuriy Dunko: Or write st correct and discribe for what it need ... or remove it! 
                symbolTable = readClass(SymbolTable.class, archive.get(ma));
            } else {
                ClassFile rc = readClass(ClassFile.class, archive.get(ma));
                classes.put(rc.getName(), rc);
            }
        }

        validate();
    }

    public final void write(OutputStream os) throws IOException {
        validate();

        Map<String, byte[]> archive = new HashMap<String, byte[]>();

        for (String ma : classes.keySet()) {
            archive.put(ma + CLASS_EXTENSION, writeClass(classes.get(ma)));
        }

    	// TODO: Yuriy Dunko: Or write st correct and discribe for what it need ... or remove it! 
//        archive.put(SYMBOL_TABLE_NAME, writeClass(symbolTable));

        ArchiveUtil.writeArchive(archive, os);
    }

    private void validate() throws RuntimeException {
        if (classes.isEmpty()) {
            throw new CodeExecuteException("Cannot serialize, because no classes "
                    + "for serialization");
        }

    	// TODO: Yuriy Dunko: Or write st correct and discribe for what it need ... or remove it!
//        if (symbolTable == null) {
//            throw new CodeExecuteException("Cannot serialize, because symbol table "
//                    + "was not be initialized");
//        }
    }

    @SuppressWarnings("unchecked")
	private <T> T readClass(Class<T> c, byte[] data) throws IOException {
        ByteArrayInputStream bin = new ByteArrayInputStream(data);
        T t;
        // binary serialisation
//        ObjectInputStream ois = new ObjectInputStream(bin);
//
//        try {
//            t = (T) ois.readObject();
//        } catch (ClassNotFoundException ex) {
//            throw new IOException(ex);
//        }
//        ois.close();

        // xml serialisation
//        XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(bin));
//        t = (T) decoder.readObject();
//        decoder.close();
        // simple serialisation
        try {
            t = new Persister().read(c, bin);
        } catch (Exception ex) {
            throw new IOException(ex);
        }
        return t;
    }

    private <T> byte[] writeClass(T classFile) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        // binary serialisation
//        ObjectOutputStream oos = new ObjectOutputStream(bout);
//        oos.writeObject(classFile);
//        oos.close();

        // xml serialisation
//        XMLEncoder encoder = new XMLEncoder(new BufferedOutputStream(bout));
//        encoder.writeObject(classFile);
//        encoder.close();
        // simple serialisation
        try {
            new Persister().write(classFile, bout);
        } catch (Exception ex) {
            throw new IOException(ex);
        }

        return bout.toByteArray();
    }
}
